package com.hzw.saas.common.storage.service.impl;

import java.io.*;
import java.nio.file.StandardCopyOption;

import com.hzw.saas.common.storage.constants.StorageIoConsts;
import com.hzw.saas.common.storage.enums.PathTypeEnum;
import com.hzw.saas.common.storage.pojo.FileStorage;
import com.hzw.saas.common.storage.service.IFileStorageService;
import com.hzw.saas.common.storage.util.StorageHubUtil;
import com.hzw.saas.common.util.PathUtils;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import cn.hutool.core.date.SystemClock;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 文件存储默认实现
 *
 * @author zzl
 * @since 04/20/2021
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class FileStorageServiceImpl implements IFileStorageService {

    @Override
    public File getFile(String path) {
        Assert.isTrue(StrUtil.isNotBlank(path), "get file error: path is blank");
        return FileUtil.file(path);
    }

    @Override
    public File getFile(String supPath, String fileName) {
        Assert.isTrue(StrUtil.isNotBlank(fileName), "get file error: fileId is blank");
        String path = PathUtils.connectPath(supPath, fileName);
        return this.getFile(path);
    }

    @Override
    public boolean exist(String path) {
        File file = this.getFile(path);
        return FileUtil.isFile(file);
    }

    @Override
    public boolean exist(String supPath, String fileName) {
        String path = PathUtils.connectPath(supPath, fileName);
        return this.exist(path);
    }

    @Override
    public long getLength(String path) {
        File file = this.getFile(path);
        return StorageHubUtil.size(file);
    }

    @Override
    public long getLength(String supPath, String fileName) {
        String path = PathUtils.connectPath(supPath, fileName);
        return this.getLength(path);
    }

    @Override
    public String getDigest(String path, String digestType) {
        File file = this.getFile(path);
        return StorageHubUtil.calcDigest(file, digestType);
    }

    @Override
    public String getDigest(String supPath, String fileName, String digestType) {
        String path = PathUtils.connectPath(supPath, fileName);
        return this.getDigest(path, digestType);
    }

    @Override
    public File mkdir(String path) {
        log.debug("create folder ({})...", path);
        File folder = FileUtil.file(path);
        if (FileUtil.isDirectory(folder))
            return folder;
        folder = FileUtil.mkdir(folder);
        log.info("create folder ({}) completed.", path);
        return folder;
    }

    @Override
    public File mkdir(String supPath, String folderName) throws IORuntimeException {
        String path = PathUtils.connectPath(supPath, folderName);
        return this.mkdir(path);
    }

    @Override
    public File rename(String supPath, String srcName, String tarName, ConflictModeEnum mode) throws IORuntimeException {
        log.debug("rename file (folder: {}, {} -> {})...", supPath, srcName, tarName);
        File srcFile = this.getFile(supPath, srcName);
        File tarFile = this.getFile(supPath, tarName);
        // check source exists
        StorageHubUtil.assertExist(srcFile);
        // check unchanged name
        if (FileUtil.equals(srcFile, tarFile)) {
            log.debug("rename file cancel, name unchanged: {}", srcFile);
            return srcFile;
        }
        // deal with target conflict
        PathTypeEnum pathType = PathTypeEnum.instance(srcFile);
        tarFile = StorageHubUtil.doConflictName(tarFile, pathType, true, mode);
        // rename
        // 计时开始
        long start = SystemClock.now();
        try {
            FileUtil.rename(srcFile, tarFile.getName(), true);
        } catch (IORuntimeException e) {
            // StorageUtil.silentDelete(tarFile);
            throw e;
        } catch (Exception e) {
            // StorageUtil.silentDelete(tarFile);
            throw new IORuntimeException(e);
        }
        // 计时结束
        long cost = SystemClock.now() - start;
        log.debug("rename file (folder: {}, {} -> {}) finished.", supPath, srcName, tarName);
        return tarFile;

    }

    @Override
    public File rename(String srcPath, String tarName, ConflictModeEnum mode) throws IORuntimeException {
        String supPath = FileUtil.getParent(srcPath, 1);
        String srcName = FileUtil.getName(srcPath);
        return this.rename(supPath, srcName, tarName, mode);
    }

    @Override
    public File copy(String srcPath, String tarPath, ConflictModeEnum mode) throws IORuntimeException {
        File srcFile = this.getFile(srcPath);
        File tarFile = this.getFile(tarPath);
        tarFile = this.copy(srcFile, tarFile, mode);
        return tarFile;
    }


    @Override
    public File move(String srcPath, String tarPath, ConflictModeEnum mode) throws IORuntimeException {
        File srcFile = this.getFile(srcPath);
        File tarFile = this.getFile(tarPath);
        tarFile = this.move(srcFile, tarFile, mode);
        return tarFile;
    }

    @Override
    public void delete(String supPath, String fileName, boolean rf) throws IORuntimeException {
        String path = PathUtils.connectPath(supPath, fileName);
        this.delete(path, rf);
    }

    @Override
    public void delete(String path, boolean rf) throws IORuntimeException {
        log.debug("delete file: {}", path);
        File file = FileUtil.file(path);
        // check source exists
        if (FileUtil.isDirectory(file) && !FileUtil.isDirEmpty(file) && !rf) {
            log.debug("folder is not empty && rf=false, throw exception.d");
            throw new IORuntimeException("目录不为空：" + file);
        }
        // start time
        long start = SystemClock.now();
        PathUtil.del(file.toPath());
        // end time
        long costTime = SystemClock.now() - start;
        log.info("delete file ({}) completed, cost {}ms", file, costTime);
    }

    @Override
    public File write(FileStorage tarFileInfo, InputStream srcIns, ConflictModeEnum mode) throws IORuntimeException {
        // 参数校验
        Assert.notNull(srcIns, "save file: input stream is null.");
        Assert.notNull(tarFileInfo, "save file: fileInfo is null");
        // 目标文件
        File file = this.getFile(tarFileInfo.getSuperPath(), tarFileInfo.getFileName());
        log.debug("save the file by input stream: {}", file);
        // 重名检查
        file = StorageHubUtil.doConflictName(file, PathTypeEnum.FILE, true, mode);
        tarFileInfo.setFileName(FileUtil.getName(file));
        // 保存
        long costTime = this.write(srcIns, file, tarFileInfo.length());
        // 校验目标文件
        StorageHubUtil.checkTarLength(file, tarFileInfo.getLength(), true);
        StorageHubUtil.checkTarDigest(file, tarFileInfo.getDigest(), tarFileInfo.getDigestType(), true);
        log.info("save the file({}) completed, cost {}ms", file, costTime);
        return file;
    }

    @Override
    public File write(FileStorage tarFileInfo, File srcFile, ConflictModeEnum mode) throws IORuntimeException {
        // 校验参数
        Assert.isTrue(srcFile.isFile(), "save file: source file is not exist.");
        Assert.notNull(tarFileInfo, "save file: fileInfo is null");
        // 校验源
        StorageHubUtil.assertExist(srcFile);
        StorageHubUtil.checkSrcLength(srcFile, tarFileInfo.getLength());
        StorageHubUtil.checkSrcDigest(srcFile, tarFileInfo.getDigest(), tarFileInfo.getDigestType());
        // 目标文件
        File outFile = this.getFile(tarFileInfo.getSuperPath(), tarFileInfo.getFileName());
        log.debug("save the file by source file: {}", outFile);
        // 重名检查
        outFile = StorageHubUtil.doConflictName(outFile, PathTypeEnum.FILE, true, mode);
        tarFileInfo.setFileName(FileUtil.getName(outFile));
        long costTime = this.copy(srcFile, outFile);
        // 校验目标
        StorageHubUtil.checkTarLength(outFile, tarFileInfo.getLength(), true);
        StorageHubUtil.checkTarDigest(outFile, tarFileInfo.getDigest(), tarFileInfo.getDigestType(), true);
        log.info("save the file({}) completed, cost {}ms", outFile, costTime);
        return outFile;
    }

    @Override
    public File write(FileStorage tarFileInfo, byte[] data, ConflictModeEnum mode) throws IORuntimeException {
        // 参数校验
        Assert.notNull(data, "save file: data to save is null.");
        Assert.notNull(tarFileInfo, "save file: file info is null");
        // 校验源
        StorageHubUtil.checkSrcLength(data, tarFileInfo.getLength());
        StorageHubUtil.checkSrcDigest(data, tarFileInfo.getDigest(), tarFileInfo.getDigestType());
        // 目标文件
        File file = this.getFile(tarFileInfo.getSuperPath(), tarFileInfo.getFileName());
        log.debug("save the file by bytes: {}", file);
        // 重名检查
        file = StorageHubUtil.doConflictName(file, PathTypeEnum.FILE, true, mode);
        tarFileInfo.setFileName(file.getName());
        // 保存
        long costTime = this.write(data, file);
        // 校验目标
        StorageHubUtil.checkTarLength(file, tarFileInfo.getLength(), true);
        StorageHubUtil.checkTarDigest(file, tarFileInfo.getDigest(), tarFileInfo.getDigestType(), true);
        log.info("save the file({}) completed, cost {}ms", file, costTime);
        return file;
    }

    @Override
    public void read(String path, OutputStream os, long skipBytes) throws IORuntimeException {
        File file = this.getFile(path);
        if (!FileUtil.isFile(file))
            log.info("download file: {}", file);
        // check source exists
        StorageHubUtil.assertExist(file);
        // check skip
        long length = file.length();
        if (skipBytes >= length)
            return;
        try (BufferedInputStream is = FileUtil.getInputStream(file)) {
            // start time
            long start = SystemClock.now();
            // download
            skipBytes = skipBytes < 0 ? 0 : skipBytes;
            int as = 0;
            do {
                as += is.skip(skipBytes - as);
            } while (as < skipBytes);
            IoUtil.copy(is, os);
            // end time
            long costTime = SystemClock.now() - start;
            log.info("download file ({}) completed, cost {}ms", file, costTime);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    private long write(InputStream in, File tarFile, long fileLength) {
        // 计时开始
        long start = SystemClock.now();
        try (OutputStream out = FileUtil.getOutputStream(tarFile)) {
            byte[] buffer = new byte[StorageIoConsts.DEFAULT_BUFFER_SIZE];
            long size = 0;
            for (int readSize; (readSize = in.read(buffer)) != StorageIoConsts.EOF; ) {
                // 指定读取长度
                if (fileLength > 0) {
                    long remainSize = fileLength - size;
                    if (remainSize <= 0)
                        break;
                    if (remainSize < readSize)
                        readSize = (int) remainSize;
                }
                out.write(buffer, 0, readSize);
                size += readSize;
                out.flush();
            }
        } catch (IORuntimeException e) {
            StorageHubUtil.silentDelete(tarFile);
            throw e;
        } catch (Exception e) {
            StorageHubUtil.silentDelete(tarFile);
            throw new IORuntimeException(e);
        }
        // 计时结束
        return SystemClock.now() - start;
    }

    private long write(byte[] bytes, File tarFile) {
        // 计时开始
        long start = SystemClock.now();
        // 保存
        try {
            FileUtil.writeBytes(bytes, tarFile);
        } catch (IORuntimeException e) {
            StorageHubUtil.silentDelete(tarFile);
            throw e;
        } catch (Exception e) {
            StorageHubUtil.silentDelete(tarFile);
            throw new IORuntimeException(e);
        }
        // 计时结束
        return SystemClock.now() - start;
    }

    private File copy(File srcFile, File tarFile, ConflictModeEnum mode) throws IORuntimeException {
        // TODO: ZZL@06/29/2021 增加目录复制
        log.debug("copy the file: {} -> {}", srcFile, tarFile);
        // check source exists
        StorageHubUtil.assertExist(srcFile);
        // check copy in same dir
        if (FileUtil.equals(srcFile, tarFile)) {
            log.debug("copy file in same dir: {}", srcFile);
            mode = ConflictModeEnum.RENAME_SRC;
        }
        // deal with target conflict
        PathTypeEnum pathType = PathTypeEnum.instance(srcFile);
        tarFile = StorageHubUtil.doConflictName(tarFile, pathType, true, mode);
        long costTime = this.copy(srcFile, tarFile);
        log.info("copy the file ({} -> {}) completed, cost {}ms", srcFile, tarFile, costTime);
        return tarFile;
    }

    private long copy(File srcFile, File tarFile) {
        // 源与目标同文件检查
        if (FileUtil.equals(srcFile, tarFile)) {
            log.debug("copy file: Files '{}' and '{}' are equal", srcFile, tarFile);
            return 0;
        }
        // 计时开始
        long start = SystemClock.now();
        // 保存
        try {
            FileUtil.copyFile(srcFile, tarFile, StandardCopyOption.REPLACE_EXISTING);
        } catch (IORuntimeException e) {
            StorageHubUtil.silentDelete(tarFile);
            throw e;
        } catch (Exception e) {
            StorageHubUtil.silentDelete(tarFile);
            throw new IORuntimeException(e);
        }
        // 计时结束
        return SystemClock.now() - start;
    }

    private File move(File srcFile, File tarFile, ConflictModeEnum mode) throws IORuntimeException {
        log.debug("move the file: {} -> {}", srcFile, tarFile);
        // check source exists
        StorageHubUtil.assertExist(srcFile);
        // check move in same dir
        if (FileUtil.equals(srcFile, tarFile)) {
            log.debug("move file cancel, same target dir: {}", srcFile);
            return srcFile;
        }
        // deal with target conflict
        PathTypeEnum pathType = PathTypeEnum.instance(srcFile);
        tarFile = StorageHubUtil.doConflictName(tarFile, pathType, true, mode);
        long costTime = this.move(srcFile, tarFile);
        log.info("move the file ({} -> {}) completed, cost {}ms", srcFile, tarFile, costTime);
        return tarFile;
    }

    private long move(File srcFile, File tarFile) {
        // source equals target
        if (FileUtil.equals(srcFile, tarFile)) {
            log.debug("move file: Files '{}' and '{}' are equal", srcFile, tarFile);
            return 0;
        }
        // start time
        long start = SystemClock.now();
        // move
        try {
            FileUtil.move(srcFile, tarFile, true);
        } catch (IORuntimeException e) {
            StorageHubUtil.silentDelete(tarFile);
            throw e;
        } catch (Exception e) {
            StorageHubUtil.silentDelete(tarFile);
            throw new IORuntimeException(e);
        }
        // end time
        return SystemClock.now() - start;
    }
}
